///----------- ----------- ----------- ----------- ----------- ----------- -----------
/// <copyright file="DeflaterZLib.cs" company="Microsoft">
///     Copyright (c) Microsoft Corporation.  All rights reserved.
/// </copyright>                               
///
/// <owner>gpaperin</owner>
///----------- ----------- ----------- ----------- ----------- ----------- -----------

using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Runtime.InteropServices;
using System.Security;
using System.Threading;

using ZErrorCode = System.IO.Compression.ZLibNative.ErrorCode;
using ZFlushCode = System.IO.Compression.ZLibNative.FlushCode;


namespace System.IO.Compression {

#if FEATURE_NETCORE
[SecuritySafeCritical]
#endif
internal class DeflaterZLib : IDeflater {

    private ZLibNative.ZLibStreamHandle _zlibStream;
    private GCHandle _inputBufferHandle;
    private bool _isDisposed;

    // non-zero indicates a valid handle
    private int _isValid;

    // Note, DeflateStream or the deflater do not try to be thread safe.
    // The lock is just used to make writing to unmanaged structures atomic to make sure
    // that they do not get inconsistent fields that may lead to an unmanaged memory violation.
    // To prevent *managed* buffer corruption or other weird behaviour users need to synchronise
    // on the stream explicitly.
    private readonly Object syncLock = new Object();

    #region exposed members
   
    internal DeflaterZLib()

        : this(CompressionLevel.Optimal) {
    }

    internal DeflaterZLib(CompressionLevel compressionLevel) {

        ZLibNative.CompressionLevel zlibCompressionLevel;
        int windowBits;
        int memLevel;
        ZLibNative.CompressionStrategy strategy;

        switch (compressionLevel) {

            // Note that ZLib currently exactly correspond to the optimal values.
            // However, we have determined the optimal values by intependent measurements across
            // a range of all possible ZLib parameters and over a set of different data.
            // We stress that by using explicitly the values obtained by the measurements rather than
            // ZLib defaults even if they happened to be the same.
            // For ZLib 1.2.3 we have (copied from ZLibNative.cs):
            // ZLibNative.CompressionLevel.DefaultCompression = 6;
            // ZLibNative.Deflate_DefaultWindowBits = -15;
            // ZLibNative.Deflate_DefaultMemLevel = 8;

            
            case CompressionLevel.Optimal:
                zlibCompressionLevel = (ZLibNative.CompressionLevel) 6;
                windowBits = -15;
                memLevel = 8; 
                strategy = ZLibNative.CompressionStrategy.DefaultStrategy;
                break;

            case CompressionLevel.Fastest:
                zlibCompressionLevel = (ZLibNative.CompressionLevel) 1;
                windowBits = -15;
                memLevel = 8; 
                strategy = ZLibNative.CompressionStrategy.DefaultStrategy;
                break;

            case CompressionLevel.NoCompression:
                zlibCompressionLevel = (ZLibNative.CompressionLevel) 0;
                windowBits = -15;
                memLevel = 7; 
                strategy = ZLibNative.CompressionStrategy.DefaultStrategy;
                break;

            default:
                throw new ArgumentOutOfRangeException("compressionLevel");
        }

        _isDisposed = false;
        DeflateInit(zlibCompressionLevel, windowBits, memLevel, strategy);        
    }

    ~DeflaterZLib()
    {
        if (Environment.HasShutdownStarted)
            return;
            
        Dispose(false);
    }    

    void IDisposable.Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    [SecuritySafeCritical]
    protected virtual void Dispose(bool disposing) {

        if (!_isDisposed) {
            if (disposing)
                _zlibStream.Dispose();
           
            if (_inputBufferHandle.IsAllocated)
                DeallocateInputBufferHandle();

            _isDisposed = true;
        }
    }

    private bool NeedsInput() {
        // Convenience method to call NeedsInput privately without a cast.
        return ((IDeflater) this).NeedsInput();
    }

    [SecuritySafeCritical]
    bool IDeflater.NeedsInput() {        
        return 0 == _zlibStream.AvailIn;
    }

    [SecuritySafeCritical]
    void IDeflater.SetInput(byte[] inputBuffer, int startIndex, int count) {

        Contract.Assert(NeedsInput(), "We have something left in previous input!");
        Contract.Assert(null != inputBuffer);
        Contract.Assert(startIndex >= 0 && count >= 0 && count + startIndex <= inputBuffer.Length);
        Contract.Assert(!_inputBufferHandle.IsAllocated);

        if (0 == count)
            return;

        lock (syncLock) {

            _inputBufferHandle = GCHandle.Alloc(inputBuffer, GCHandleType.Pinned);
            _isValid = 1;
        
            _zlibStream.NextIn = _inputBufferHandle.AddrOfPinnedObject() + startIndex;
            _zlibStream.AvailIn = (uint) count;
        }
    }

    [SecuritySafeCritical]
    int IDeflater.GetDeflateOutput(byte[] outputBuffer) {

        Contract.Ensures(Contract.Result<int>() >= 0 && Contract.Result<int>() <= outputBuffer.Length);

        Contract.Assert(null != outputBuffer, "Can't pass in a null output buffer!");
        Contract.Assert(!NeedsInput(), "GetDeflateOutput should only be called after providing input");
        Contract.Assert(_inputBufferHandle.IsAllocated);

        try {
            int bytesRead;
            ReadDeflateOutput(outputBuffer, ZFlushCode.NoFlush, out bytesRead);
            return bytesRead;

        } finally {
            // Before returning, make sure to release input buffer if necesary:
            if (0 == _zlibStream.AvailIn && _inputBufferHandle.IsAllocated)
                DeallocateInputBufferHandle();
        }
    }

    private unsafe ZErrorCode ReadDeflateOutput(byte[] outputBuffer, ZFlushCode flushCode, out int bytesRead) {

        lock (syncLock) {

            fixed (byte* bufPtr = outputBuffer)
            {
                _zlibStream.NextOut = (IntPtr) bufPtr;
                _zlibStream.AvailOut = (uint) outputBuffer.Length;

                ZErrorCode errC = Deflate(flushCode);
                bytesRead = outputBuffer.Length - (int) _zlibStream.AvailOut;

                return errC;
            }
        }
    }

    bool IDeflater.Finish(byte[] outputBuffer, out int bytesRead) {

        Contract.Assert(null != outputBuffer, "Can't pass in a null output buffer!");
        Contract.Assert(NeedsInput(), "We have something left in previous input!");
        Contract.Assert(!_inputBufferHandle.IsAllocated);
            
        // Note: we require that NeedsInput() == true, i.e. that 0 == _zlibStream.AvailIn.
        // If there is still input left we should never be getting here; instead we
        // should be calling GetDeflateOutput.

        ZErrorCode errC = ReadDeflateOutput(outputBuffer, ZFlushCode.Finish, out bytesRead);
        return errC == ZErrorCode.StreamEnd;            
    }

    #endregion  // exposed functions
         

    #region helpers & native call wrappers

    private void DeallocateInputBufferHandle() {

        Contract.Assert(_inputBufferHandle.IsAllocated);

        lock(syncLock) {
            _zlibStream.AvailIn = 0;
            _zlibStream.NextIn = ZLibNative.ZNullPtr;

            if (Interlocked.Exchange(ref _isValid, 0) != 0) {
                _inputBufferHandle.Free();
            }
        }
    }

    [SecuritySafeCritical]
    private void DeflateInit(ZLibNative.CompressionLevel compressionLevel, int windowBits, int memLevel,
                             ZLibNative.CompressionStrategy strategy) {

        ZErrorCode errC;
        try {
            errC = ZLibNative.CreateZLibStreamForDeflate(out _zlibStream, compressionLevel,
                                                         windowBits, memLevel, strategy);
        } catch (Exception cause) {
            throw new ZLibException(SR.GetString(SR.ZLibErrorDLLLoadError), cause);
        }

        switch (errC) {

            case ZErrorCode.Ok:
                return;

            case ZErrorCode.MemError:
                throw new ZLibException(SR.GetString(SR.ZLibErrorNotEnoughMemory), "deflateInit2_", (int) errC, _zlibStream.GetErrorMessage());

            case ZErrorCode.VersionError:
                throw new ZLibException(SR.GetString(SR.ZLibErrorVersionMismatch), "deflateInit2_", (int) errC, _zlibStream.GetErrorMessage());

            case ZErrorCode.StreamError:
                throw new ZLibException(SR.GetString(SR.ZLibErrorIncorrectInitParameters), "deflateInit2_", (int) errC, _zlibStream.GetErrorMessage());

            default:
                throw new ZLibException(SR.GetString(SR.ZLibErrorUnexpected), "deflateInit2_", (int) errC, _zlibStream.GetErrorMessage());
        }
    }
   

    [SecuritySafeCritical]
    private ZErrorCode Deflate(ZFlushCode flushCode) {

        ZErrorCode errC;
        try {
            errC = _zlibStream.Deflate(flushCode);
        } catch (Exception cause) {
            throw new ZLibException(SR.GetString(SR.ZLibErrorDLLLoadError), cause);
        }

        switch (errC) {

            case ZErrorCode.Ok:
            case ZErrorCode.StreamEnd:
                return errC;

            case ZErrorCode.BufError:
                return errC;  // This is a recoverable error
            
            case ZErrorCode.StreamError:
                throw new ZLibException(SR.GetString(SR.ZLibErrorInconsistentStream), "deflate", (int) errC, _zlibStream.GetErrorMessage());

            default:
                throw new ZLibException(SR.GetString(SR.ZLibErrorUnexpected), "deflate", (int) errC, _zlibStream.GetErrorMessage());
        }        
    }

    #endregion  // helpers & native call wrappers

}  // internal class DeflaterZLib
}  // namespace System.IO.Compression

// file DeflaterZLib.cs
